/*!
 * Ladda
 * http://lab.hakim.se/ladda
 * MIT licensed
 *
 * Copyright (C) 2016 Hakim El Hattab, http://hakim.se
 */
(function(root, factory) {
        'use strict';

        // CommonJS
        if (typeof exports === 'object') {
            module.exports = factory(require('spin.js'));
        }
        // AMD module
        else if (typeof define === 'function' && define.amd) {
            define(['spin'], factory);
        }
        // Browser global
        else {
            root.Ladda = factory(root.Spinner);
        }

    }
    (this, function(Spinner) {
        'use strict';

        // All currently instantiated instances of Ladda
        var ALL_INSTANCES = [];

        /**
         * Creates a new instance of Ladda which wraps the
         * target button element.
         *
         * @return An API object that can be used to control
         * the loading animation state.
         */
        function create(button) {

            if (typeof button === 'undefined') {
                console.warn("Ladda button target must be defined.");
                return;
            }

            // The button must have the class "ladda-button"
            if (!/ladda-button/i.test(button.className)) {
                button.className += ' ladda-button';
            }

            // Style is required, default to "expand-right"
            if (!button.hasAttribute('data-style')) {
                button.setAttribute('data-style', 'expand-right');
            }

            // The text contents must be wrapped in a ladda-label
            // element, create one if it doesn't already exist
            if (!button.querySelector('.ladda-label')) {
                var laddaLabel = document.createElement('span');
                laddaLabel.className = 'ladda-label';
                wrapContent(button, laddaLabel);
            }

            // The spinner component
            var spinner,
                spinnerWrapper = button.querySelector('.ladda-spinner');

            // Wrapper element for the spinner
            if (!spinnerWrapper) {
                spinnerWrapper = document.createElement('span');
                spinnerWrapper.className = 'ladda-spinner';
            }

            button.appendChild(spinnerWrapper);

            // Timer used to delay starting/stopping
            var timer;

            var instance = {

                /**
                 * Enter the loading state.
                 */
                start: function() {

                    // Create the spinner if it doesn't already exist
                    if (!spinner) {
                        spinner = createSpinner(button);
                    }

                    button.disabled = true;
                    button.setAttribute('data-loading', '');

                    clearTimeout(timer);
                    spinner.spin(spinnerWrapper);

                    this.setProgress(0);

                    return this; // chain

                },

                /**
                 * Enter the loading state, after a delay.
                 */
                startAfter: function(delay) {

                    clearTimeout(timer);
                    timer = setTimeout(function() {
                        instance.start();
                    }, delay);

                    return this; // chain

                },

                /**
                 * Exit the loading state.
                 */
                stop: function() {

                    if (instance.isLoading()) {
                        button.disabled = false;
                        button.removeAttribute('data-loading');
                    }

                    // Kill the animation after a delay to make sure it
                    // runs for the duration of the button transition
                    clearTimeout(timer);

                    if (spinner) {
                        timer = setTimeout(function() {
                            spinner.stop();
                        }, 1000);
                    }

                    return this; // chain

                },

                /**
                 * Toggle the loading state on/off.
                 */
                toggle: function() {
                    return this.isLoading() ? this.stop() : this.start();
                },

                /**
                 * Sets the width of the visual progress bar inside of
                 * this Ladda button
                 *
                 * @param {Number} progress in the range of 0-1
                 */
                setProgress: function(progress) {

                    // Cap it
                    progress = Math.max(Math.min(progress, 1), 0);

                    var progressElement = button.querySelector('.ladda-progress');

                    // Remove the progress bar if we're at 0 progress
                    if (progress === 0 && progressElement && progressElement.parentNode) {
                        progressElement.parentNode.removeChild(progressElement);
                    } else {
                        if (!progressElement) {
                            progressElement = document.createElement('div');
                            progressElement.className = 'ladda-progress';
                            button.appendChild(progressElement);
                        }

                        progressElement.style.width = ((progress || 0) * button.offsetWidth) + 'px';
                    }

                },

                /**
                 * @deprecated
                 */
                enable: function() {

                    return this.stop();

                },

                /**
                 * @deprecated
                 */
                disable: function() {

                    this.stop();
                    button.disabled = true;

                    return this; // chain

                },

                isLoading: function() {

                    return button.hasAttribute('data-loading');

                },

                remove: function() {

                    clearTimeout(timer);

                    button.disabled = false;
                    button.removeAttribute('data-loading');

                    if (spinner) {
                        spinner.stop();
                        spinner = null;
                    }

                    ALL_INSTANCES.splice(ALL_INSTANCES.indexOf(instance), 1);

                }

            };

            ALL_INSTANCES.push(instance);

            return instance;

        }

        /**
         * Get the first ancestor node from an element, having a
         * certain type.
         *
         * @param elem An HTML element
         * @param type an HTML tag type (uppercased)
         *
         * @return An HTML element
         */
        function getAncestorOfTagType(elem, type) {

            while (elem.parentNode && elem.tagName !== type) {
                elem = elem.parentNode;
            }

            return (type === elem.tagName) ? elem : undefined;

        }

        /**
         * Returns a list of all inputs in the given form that
         * have their `required` attribute set.
         *
         * @param form The from HTML element to look in
         *
         * @return A list of elements
         */
        function getRequiredFields(form) {

            var requirables = ['input', 'textarea', 'select'];
            var inputs = [];

            requirables.forEach(function(r) {
                var candidates = form.getElementsByTagName(r);

                for (var j = 0; j < candidates.length; j++) {
                    // legacy browsers don't support required property
                    if (candidates[j].hasAttribute('required')) {
                        inputs.push(candidates[j]);
                    }
                }
            });

            return inputs;

        }


        /**
         * Binds the target buttons to automatically enter the
         * loading state when clicked.
         *
         * @param target Either an HTML element or a CSS selector.
         * @param options
         *          - timeout Number of milliseconds to wait before
         *            automatically cancelling the animation.
         */
        function bind(target, options) {

            var targets;

            if (typeof target === 'string') {
                targets = document.querySelectorAll(target);
            } else if (typeof target === 'object') {
                targets = [target];
            } else {
                throw new Error('target must be string or object');
            }

            options = options || {};

            for (var i = 0; i < targets.length; i++) {
                bindElement(targets[i], options);
            }

        }

        /**
         * Stops ALL current loading animations.
         */
        function stopAll() {

            for (var i = 0, len = ALL_INSTANCES.length; i < len; i++) {
                ALL_INSTANCES[i].stop();
            }

        }

        function createSpinner(button) {

            var height = button.offsetHeight,
                spinnerColor,
                spinnerLines;

            if (height === 0) {
                // We may have an element that is not visible so
                // we attempt to get the height in a different way
                height = parseFloat(window.getComputedStyle(button).height);
            }

            // If the button is tall we can afford some padding
            if (height > 32) {
                height *= 0.8;
            }

            // Prefer an explicit height if one is defined
            if (button.hasAttribute('data-spinner-size')) {
                height = parseInt(button.getAttribute('data-spinner-size'), 10);
            }

            // Allow buttons to specify the color of the spinner element
            if (button.hasAttribute('data-spinner-color')) {
                spinnerColor = button.getAttribute('data-spinner-color');
            }

            // Allow buttons to specify the number of lines of the spinner
            if (button.hasAttribute('data-spinner-lines')) {
                spinnerLines = parseInt(button.getAttribute('data-spinner-lines'), 10);
            }

            var radius = height * 0.2,
                length = radius * 0.6,
                width = radius < 7 ? 2 : 3;

            return new Spinner({
                color: spinnerColor || '#fff',
                lines: spinnerLines || 12,
                radius: radius,
                length: length,
                width: width,
                zIndex: 'auto',
                top: 'auto',
                left: 'auto',
                className: ''
            });

        }

        function wrapContent(node, wrapper) {

            var r = document.createRange();
            r.selectNodeContents(node);
            r.surroundContents(wrapper);
            node.appendChild(wrapper);

        }

        function bindElement(element, options) {
            if (typeof element.addEventListener !== 'function') {
                return;
            }

            var instance = create(element);
            var timeout = -1;

            element.addEventListener('click', function() {

                // If the button belongs to a form, make sure all the
                // fields in that form are filled out
                var valid = true;
                var form = getAncestorOfTagType(element, 'FORM');

                if (typeof form !== 'undefined' && !form.hasAttribute('novalidate')) {
                    // Modern form validation
                    if (typeof form.checkValidity === 'function') {
                        valid = form.checkValidity();
                    }
                    // Fallback to manual validation for old browsers
                    else {
                        var requireds = getRequiredFields(form);
                        for (var i = 0; i < requireds.length; i++) {
                            var field = requireds[i];

                            // The input type property will always return "text" for email and url fields in IE 9.
                            // Note that emulating IE 9 in IE 11 will also return "text" for the type attribute,
                            // but the actual IE 9 browser will return the correct attribute.
                            var fieldType = field.getAttribute('type');

                            if (field.value.replace(/^\s+|\s+$/g, '') === '') {
                                valid = false;
                            }

                            // Radiobuttons and Checkboxes need to be checked for the "checked" attribute
                            if ((fieldType === 'checkbox' || fieldType === 'radio') && !field.checked) {
                                valid = false;
                            }

                            // Email field validation
                            if (fieldType === 'email') {
                                // regex from https://stackoverflow.com/a/7786283/1170489
                                valid = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i.test(field.value);
                            }

                            // URL field validation
                            if (fieldType === 'url') {
                                // regex from https://stackoverflow.com/a/10637803/1170489
                                valid = /^([a-z]([a-z]|\d|\+|-|\.)*):(\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?((\[(|(v[\da-f]{1,}\.(([a-z]|\d|-|\.|_|~)|[!\$&'\(\)\*\+,;=]|:)+))\])|((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=])*)(:\d*)?)(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*|(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)|((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)|((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)){0})(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(field.value);
                            }

                            if (!valid) {
                                break;
                            }
                        }
                    }
                }

                if (valid) {
                    // This is asynchronous to avoid an issue where disabling
                    // the button prevents forms from submitting
                    instance.startAfter(1);

                    // Set a loading timeout if one is specified
                    if (typeof options.timeout === 'number') {
                        clearTimeout(timeout);
                        timeout = setTimeout(instance.stop, options.timeout);
                    }

                    // Invoke callbacks
                    if (typeof options.callback === 'function') {
                        options.callback.apply(null, [instance]);
                    }
                }

            }, false);

        }

        // Public API
        return {

            bind: bind,
            create: create,
            stopAll: stopAll

        };

    }));